/*
* Audacity: A Digital Audio Editor
*/
#include "trackeditactionscontroller.h"
#include "project/internal/audacityproject.h"
#include "trackediterrors.h"

#include "global/translation.h"
#include "global/defer.h"

using namespace muse;
using namespace au::trackedit;
using namespace muse::async;
using namespace muse::actions;

static const ActionCode TRACKEDIT_COPY_CODE("action://trackedit/copy");
static const ActionCode TRACKEDIT_CUT_CODE("action://trackedit/cut");
static const ActionCode TRACKEDIT_UNDO("action://trackedit/undo");
static const ActionCode TRACKEDIT_REDO("action://trackedit/redo");
static const ActionCode TRACKEDIT_DELETE_CODE("action://trackedit/delete");
static const ActionCode TRACKEDIT_CANCEL_CODE("action://trackedit/cancel");

static const ActionCode TRACKEDIT_PASTE_DEFAULT_CODE("action://trackedit/paste-default");
static const ActionCode TRACKEDIT_PASTE_OVERLAP_CODE("action://trackedit/paste-overlap");
static const ActionCode TRACKEDIT_PASTE_INSERT_CODE("action://trackedit/paste-insert");
static const ActionCode TRACKEDIT_PASTE_INSERT_ALL_TRACKS_RIPPLE_CODE("action://trackedit/paste-insert-all-tracks-ripple");

static const ActionCode SPLIT_CODE("split");
static const ActionCode SPLIT_INTO_NEW_TRACK_CODE("split-into-new-track");
static const ActionCode JOIN_CODE("join");
static const ActionCode DISJOIN_CODE("disjoin");
static const ActionCode DUPLICATE_CODE("duplicate");

static const ActionCode CUT_PER_CLIP_RIPPLE_CODE("cut-per-clip-ripple");
static const ActionCode CUT_PER_TRACK_RIPPLE_CODE("cut-per-track-ripple");
static const ActionCode CUT_ALL_TRACKS_RIPPLE_CODE("cut-all-tracks-ripple");

static const ActionCode DELETE_LEAVE_GAP_CODE("split-delete");
static const ActionCode DELETE_PER_CLIP_RIPPLE_CODE("delete-per-clip-ripple");
static const ActionCode DELETE_PER_TRACK_RIPPLE_CODE("delete-per-track-ripple");
static const ActionCode DELETE_ALL_TRACKS_RIPPLE_CODE("delete-all-tracks-ripple");

static const ActionCode CLIP_CUT_CODE("clip-cut");
static const ActionCode MULTI_CLIP_CUT_CODE("multi-clip-cut");
static const ActionCode RANGE_SELECTION_CUT_CODE("clip-cut-selected");

static const ActionCode CLIP_COPY_CODE("clip-copy");
static const ActionCode MULTI_CLIP_COPY_CODE("multi-clip-copy");
static const ActionCode RANGE_SELECTION_COPY_CODE("clip-copy-selected");

static const ActionCode CLIP_DELETE_CODE("clip-delete");
static const ActionCode MULTI_CLIP_DELETE_CODE("multi-clip-delete");
static const ActionCode RANGE_SELECTION_DELETE_CODE("clip-delete-selected");

static const ActionCode OPEN_CLIP_AND_SPEED_CODE("clip-pitch-speed-open");
static const ActionCode CLIP_RENDER_PITCH_AND_SPEED_CODE("clip-render-pitch-speed");
static const ActionCode TRACK_SPLIT("track-split");
static const ActionCode TRACK_SPLIT_AT("track-split-at");
static const ActionCode SPLIT_CLIPS_AT_SILENCES("split-clips-at-silences");
static const ActionCode SPLIT_RANGE_SELECTION_AT_SILENCES("split-range-selection-at-silences");
static const ActionCode SPLIT_RANGE_SELECTION_INTO_NEW_TRACKS("split-range-selection-into-new-tracks");
static const ActionCode SPLIT_CLIPS_INTO_NEW_TRACKS("split-clips-into-new-tracks");
static const ActionCode MERGE_SELECTED_ON_TRACK("merge-selected-on-tracks");
static const ActionCode DUPLICATE_RANGE_SELECTION_CODE("duplicate-selected");
static const ActionCode DUPLICATE_CLIPS_CODE("duplicate-clips");
static const ActionCode CLIP_SPLIT_CUT("clip-split-cut");
static const ActionCode CLIP_SPLIT_DELETE("clip-split-delete");
static const ActionCode RANGE_SELECTION_SPLIT_CUT("split-cut-selected");
static const ActionCode RANGE_SELECTION_SPLIT_DELETE("split-delete-selected");
static const ActionCode NEW_MONO_TRACK("new-mono-track");
static const ActionCode NEW_STEREO_TRACK("new-stereo-track");
static const ActionCode NEW_LABEL_TRACK("new-label-track");
static const ActionCode TRACK_DELETE("track-delete");
static const ActionCode TRACK_DUPLICATE_CODE("track-duplicate");
static const ActionCode TRACK_MOVE_UP("track-move-up");
static const ActionCode TRACK_MOVE_DOWN("track-move-down");
static const ActionCode TRACK_MOVE_TOP("track-move-top");
static const ActionCode TRACK_MOVE_BOTTOM("track-move-bottom");
static const ActionCode TRACK_SWAP_CHANNELS("track-swap-channels");
static const ActionCode TRACK_SPLIT_STEREO_TO_LR("track-split-stereo-to-lr");
static const ActionCode TRACK_SPLIT_STEREO_TO_CENTER("track-split-stereo-to-center");
static const ActionCode TRACK_CHANGE_RATE_CUSTOM("track-change-rate-custom");
static const ActionCode TRACK_MAKE_STEREO("track-make-stereo");
static const ActionCode TRACK_RESAMPLE("track-resample");

static const ActionCode TRIM_AUDIO_OUTSIDE_SELECTION("trim-audio-outside-selection");
static const ActionCode SILENCE_AUDIO_SELECTION("silence-audio-selection");

static const ActionCode STRETCH_ENABLED_CODE("stretch-clip-to-match-tempo");

static const ActionCode GROUP_CLIPS_CODE("group-clips");
static const ActionCode UNGROUP_CLIPS_CODE("ungroup-clips");

static const ActionCode SELECT_ALL("select-all");
static const ActionCode SELECT_CLEAR("clear-selection");
static const ActionCode SELECT_ALL_TRACKS("select-all-tracks");
static const ActionCode SELECT_LEFT_OF_PLAYBACK_POS("select-left-of-playback-position");
static const ActionCode SELECT_RIGHT_OF_PLAYBACK_POS("select-right-of-playback-position");
static const ActionCode SELECT_TRACK_START_TO_CURSOR("select-track-start-to-cursor");
static const ActionCode SELECT_CURSOR_TO_TRACK_END("select-cursor-to-track-end");
static const ActionCode SELECT_TRACK_START_TO_END("select-track-start-to-end");
static const ActionCode SELECT_ZERO_CROSSING("zero-cross");

static const ActionQuery AUTO_COLOR_QUERY("action://trackedit/clip/change-color-auto");
static const ActionQuery CHANGE_COLOR_QUERY("action://trackedit/clip/change-color");
static const ActionQuery TRACK_CHANGE_COLOR_QUERY("action://trackedit/track/change-color");
static const ActionQuery TRACK_CHANGE_FORMAT_QUERY("action://trackedit/track/change-format");
static const ActionQuery TRACK_CHANGE_RATE_QUERY("action://trackedit/track/change-rate");

static const ActionQuery SET_TRACK_VIEW_WAVEFORM("action://trackedit/track-view-waveform");
static const ActionQuery SET_TRACK_VIEW_SPECTROGRAM("action://trackedit/track-view-spectrogram");
static const ActionQuery SET_TRACK_VIEW_MULTI("action://trackedit/track-view-multi");
static const ActionQuery TRACK_OPEN_SPECTROGRAM_SETTINGS("action://trackedit/track-spectrogram-settings");

static const ActionCode LABEL_ADD_CODE("label-add");

static const ActionCode LABEL_DELETE_CODE("label-delete");
static const ActionCode LABEL_DELETE_MULTI_CODE("label-delete-multi");

static const ActionCode LABEL_CUT_CODE("label-cut");
static const ActionCode LABEL_CUT_MULTI_CODE("label-cut-multi");

static const ActionCode LABEL_COPY_CODE("label-copy");
static const ActionCode LABEL_COPY_MULTI_CODE("label-copy-multi");

static const ActionQuery PLAYBACK_SEEK_QUERY("action://playback/seek");

// In principle, disabled are actions that modify the data involved in playback.
static const std::vector<ActionCode> actionsDisabledDuringRecording {
    TRACKEDIT_CUT_CODE,
    CUT_PER_CLIP_RIPPLE_CODE,
    CUT_PER_TRACK_RIPPLE_CODE,
    TRACKEDIT_DELETE_CODE,
    DELETE_PER_CLIP_RIPPLE_CODE,
    DELETE_PER_TRACK_RIPPLE_CODE,
    DELETE_ALL_TRACKS_RIPPLE_CODE,
    SPLIT_CODE,
    SPLIT_INTO_NEW_TRACK_CODE,
    JOIN_CODE,
    DUPLICATE_CODE,
    CLIP_CUT_CODE,
    MULTI_CLIP_CUT_CODE,
    RANGE_SELECTION_CUT_CODE,
    CLIP_DELETE_CODE,
    MULTI_CLIP_DELETE_CODE,
    RANGE_SELECTION_DELETE_CODE,
    CLIP_RENDER_PITCH_AND_SPEED_CODE,
    TRACKEDIT_PASTE_DEFAULT_CODE,
    TRACKEDIT_PASTE_OVERLAP_CODE,
    TRACKEDIT_PASTE_INSERT_CODE,
    TRACKEDIT_PASTE_INSERT_ALL_TRACKS_RIPPLE_CODE,
    TRACK_SPLIT,
    TRACK_SPLIT_AT,
    SPLIT_CLIPS_AT_SILENCES,
    SPLIT_RANGE_SELECTION_AT_SILENCES,
    SPLIT_RANGE_SELECTION_INTO_NEW_TRACKS,
    SPLIT_CLIPS_INTO_NEW_TRACKS,
    MERGE_SELECTED_ON_TRACK,
    DUPLICATE_RANGE_SELECTION_CODE,
    DUPLICATE_CLIPS_CODE,
    CLIP_SPLIT_CUT,
    CLIP_SPLIT_DELETE,
    RANGE_SELECTION_SPLIT_CUT,
    RANGE_SELECTION_SPLIT_DELETE,
    NEW_MONO_TRACK,
    NEW_STEREO_TRACK,
    NEW_LABEL_TRACK,
    TRIM_AUDIO_OUTSIDE_SELECTION,
    SILENCE_AUDIO_SELECTION,
    TRACKEDIT_UNDO,
    TRACKEDIT_REDO,
    STRETCH_ENABLED_CODE,
    TRACK_DELETE,
    TRACK_DUPLICATE_CODE,
    TRACK_SWAP_CHANNELS,
    TRACK_SPLIT_STEREO_TO_LR,
    TRACK_SPLIT_STEREO_TO_CENTER,
    TRACK_CHANGE_RATE_CUSTOM,
    TRACK_MAKE_STEREO,
    TRACK_RESAMPLE,
    GROUP_CLIPS_CODE,
    UNGROUP_CLIPS_CODE,
};

void TrackeditActionsController::init()
{
    dispatcher()->reg(this, TRACKEDIT_COPY_CODE, this, &TrackeditActionsController::doGlobalCopy);
    dispatcher()->reg(this, TRACKEDIT_CUT_CODE, this, &TrackeditActionsController::doGlobalCut);
    dispatcher()->reg(this, TRACKEDIT_UNDO, this, &TrackeditActionsController::undo);
    dispatcher()->reg(this, TRACKEDIT_REDO, this, &TrackeditActionsController::redo);
    dispatcher()->reg(this, TRACKEDIT_DELETE_CODE, this, &TrackeditActionsController::doGlobalDelete);
    dispatcher()->reg(this, TRACKEDIT_CANCEL_CODE, this, &TrackeditActionsController::doGlobalCancel);

    dispatcher()->reg(this, TRACKEDIT_PASTE_DEFAULT_CODE, this, &TrackeditActionsController::pasteDefault);
    dispatcher()->reg(this, TRACKEDIT_PASTE_INSERT_CODE, this, &TrackeditActionsController::pasteInsert);
    dispatcher()->reg(this, TRACKEDIT_PASTE_OVERLAP_CODE, this, &TrackeditActionsController::pasteOverlap);
    dispatcher()->reg(this, TRACKEDIT_PASTE_INSERT_ALL_TRACKS_RIPPLE_CODE, this, &TrackeditActionsController::pasteInsertRipple);

    dispatcher()->reg(this, SPLIT_CODE, this, &TrackeditActionsController::doGlobalSplit);
    dispatcher()->reg(this, SPLIT_INTO_NEW_TRACK_CODE, this, &TrackeditActionsController::doGlobalSplitIntoNewTrack);
    dispatcher()->reg(this, JOIN_CODE, this, &TrackeditActionsController::doGlobalJoin);
    dispatcher()->reg(this, DISJOIN_CODE, this, &TrackeditActionsController::doGlobalDisjoin);
    dispatcher()->reg(this, DUPLICATE_CODE, this, &TrackeditActionsController::doGlobalDuplicate);

    dispatcher()->reg(this, CUT_PER_CLIP_RIPPLE_CODE, this, &TrackeditActionsController::doGlobalCutPerClipRipple);
    dispatcher()->reg(this, CUT_PER_TRACK_RIPPLE_CODE, this, &TrackeditActionsController::doGlobalCutPerTrackRipple);
    dispatcher()->reg(this, CUT_ALL_TRACKS_RIPPLE_CODE, this, &TrackeditActionsController::doGlobalCutAllTracksRipple);

    dispatcher()->reg(this, DELETE_PER_CLIP_RIPPLE_CODE, this, &TrackeditActionsController::doGlobalDeletePerClipRipple);
    dispatcher()->reg(this, DELETE_PER_TRACK_RIPPLE_CODE, this, &TrackeditActionsController::doGlobalDeletePerTrackRipple);
    dispatcher()->reg(this, DELETE_ALL_TRACKS_RIPPLE_CODE, this, &TrackeditActionsController::doGlobalDeleteAllTracksRipple);

    dispatcher()->reg(this, CLIP_CUT_CODE, this, &TrackeditActionsController::clipCut);
    dispatcher()->reg(this, MULTI_CLIP_CUT_CODE, this, &TrackeditActionsController::multiClipCut);
    dispatcher()->reg(this, RANGE_SELECTION_CUT_CODE, this, &TrackeditActionsController::rangeSelectionCut);

    dispatcher()->reg(this, CLIP_COPY_CODE, this, &TrackeditActionsController::clipCopy);
    dispatcher()->reg(this, MULTI_CLIP_COPY_CODE, this, &TrackeditActionsController::multiClipCopy);
    dispatcher()->reg(this, RANGE_SELECTION_COPY_CODE, this, &TrackeditActionsController::rangeSelectionCopy);

    dispatcher()->reg(this, CLIP_DELETE_CODE, this, &TrackeditActionsController::clipDelete);
    dispatcher()->reg(this, MULTI_CLIP_DELETE_CODE, this, &TrackeditActionsController::multiClipDelete);
    dispatcher()->reg(this, RANGE_SELECTION_DELETE_CODE, this, &TrackeditActionsController::rangeSelectionDelete);

    dispatcher()->reg(this, OPEN_CLIP_AND_SPEED_CODE, this, &TrackeditActionsController::openClipPitchAndSpeed);
    dispatcher()->reg(this, CLIP_RENDER_PITCH_AND_SPEED_CODE, this, &TrackeditActionsController::renderClipPitchAndSpeed);
    dispatcher()->reg(this, TRACK_SPLIT, this, &TrackeditActionsController::trackSplit);
    dispatcher()->reg(this, TRACK_SPLIT_AT, this, &TrackeditActionsController::tracksSplitAt);
    dispatcher()->reg(this, SPLIT_RANGE_SELECTION_AT_SILENCES, this, &TrackeditActionsController::splitRangeSelectionAtSilences);
    dispatcher()->reg(this, SPLIT_CLIPS_AT_SILENCES, this, &TrackeditActionsController::splitClipsAtSilences);
    dispatcher()->reg(this, SPLIT_RANGE_SELECTION_INTO_NEW_TRACKS, this, &TrackeditActionsController::splitRangeSelectionIntoNewTracks);
    dispatcher()->reg(this, SPLIT_CLIPS_INTO_NEW_TRACKS, this, &TrackeditActionsController::splitClipsIntoNewTracks);
    dispatcher()->reg(this, MERGE_SELECTED_ON_TRACK, this, &TrackeditActionsController::mergeSelectedOnTrack);
    dispatcher()->reg(this, DUPLICATE_RANGE_SELECTION_CODE, this, &TrackeditActionsController::duplicateSelected);
    dispatcher()->reg(this, DUPLICATE_CLIPS_CODE, this, &TrackeditActionsController::duplicateClips);
    dispatcher()->reg(this, CLIP_SPLIT_CUT, this, &TrackeditActionsController::clipSplitCut);
    dispatcher()->reg(this, CLIP_SPLIT_DELETE, this, &TrackeditActionsController::clipSplitDelete);
    dispatcher()->reg(this, RANGE_SELECTION_SPLIT_CUT, this, &TrackeditActionsController::splitCutSelected);
    dispatcher()->reg(this, RANGE_SELECTION_SPLIT_DELETE, this, &TrackeditActionsController::splitDeleteSelected);
    dispatcher()->reg(this, NEW_MONO_TRACK, this, &TrackeditActionsController::newMonoTrack);
    dispatcher()->reg(this, NEW_STEREO_TRACK, this, &TrackeditActionsController::newStereoTrack);
    dispatcher()->reg(this, NEW_LABEL_TRACK, this, &TrackeditActionsController::newLabelTrack);
    dispatcher()->reg(this, TRACK_DELETE, this, &TrackeditActionsController::deleteTracks);
    dispatcher()->reg(this, TRACK_DUPLICATE_CODE, this, &TrackeditActionsController::duplicateTracks);
    dispatcher()->reg(this, TRACK_MOVE_UP, this, &TrackeditActionsController::moveTracksUp);
    dispatcher()->reg(this, TRACK_MOVE_DOWN, this, &TrackeditActionsController::moveTracksDown);
    dispatcher()->reg(this, TRACK_MOVE_TOP, this, &TrackeditActionsController::moveTracksToTop);
    dispatcher()->reg(this, TRACK_MOVE_BOTTOM, this, &TrackeditActionsController::moveTracksToBottom);
    dispatcher()->reg(this, TRACK_SWAP_CHANNELS, this, &TrackeditActionsController::swapStereoChannels);
    dispatcher()->reg(this, TRACK_SPLIT_STEREO_TO_LR, this, &TrackeditActionsController::splitStereoToLR);
    dispatcher()->reg(this, TRACK_SPLIT_STEREO_TO_CENTER, this, &TrackeditActionsController::splitStereoToCenter);
    dispatcher()->reg(this, TRACK_CHANGE_RATE_CUSTOM, this, &TrackeditActionsController::setCustomTrackRate);
    dispatcher()->reg(this, TRACK_MAKE_STEREO, this, &TrackeditActionsController::makeStereoTrack);
    dispatcher()->reg(this, TRACK_RESAMPLE, this, &TrackeditActionsController::resampleTracks);

    dispatcher()->reg(this, TRIM_AUDIO_OUTSIDE_SELECTION, this, &TrackeditActionsController::trimAudioOutsideSelection);
    dispatcher()->reg(this, SILENCE_AUDIO_SELECTION, this, &TrackeditActionsController::silenceAudioSelection);

    dispatcher()->reg(this, STRETCH_ENABLED_CODE, this, &TrackeditActionsController::toggleStretchClipToMatchTempo);

    dispatcher()->reg(this, GROUP_CLIPS_CODE, this, &TrackeditActionsController::groupClips);
    dispatcher()->reg(this, UNGROUP_CLIPS_CODE, this, &TrackeditActionsController::ungroupClips);

    dispatcher()->reg(this, SELECT_ALL, this, &TrackeditActionsController::selectAll);
    dispatcher()->reg(this, SELECT_CLEAR, this, &TrackeditActionsController::selectNone);
    dispatcher()->reg(this, SELECT_ALL_TRACKS, this, &TrackeditActionsController::selectAllTracks);
    dispatcher()->reg(this, SELECT_LEFT_OF_PLAYBACK_POS, this, &TrackeditActionsController::selectLeftOfPlaybackPos);
    dispatcher()->reg(this, SELECT_RIGHT_OF_PLAYBACK_POS, this, &TrackeditActionsController::selectRightOfPlaybackPos);
    dispatcher()->reg(this, SELECT_TRACK_START_TO_CURSOR, this, &TrackeditActionsController::selectTrackStartToCursor);
    dispatcher()->reg(this, SELECT_CURSOR_TO_TRACK_END, this, &TrackeditActionsController::selectCursorToTrackEnd);
    dispatcher()->reg(this, SELECT_TRACK_START_TO_END, this, &TrackeditActionsController::selectTrackStartToEnd);
    dispatcher()->reg(this, SELECT_ZERO_CROSSING, this, &TrackeditActionsController::moveCursorToClosestZeroCrossing);

    dispatcher()->reg(this, AUTO_COLOR_QUERY, this, &TrackeditActionsController::setClipColor);
    dispatcher()->reg(this, CHANGE_COLOR_QUERY, this, &TrackeditActionsController::setClipColor);

    dispatcher()->reg(this, TRACK_CHANGE_COLOR_QUERY, this, &TrackeditActionsController::setTrackColor);
    dispatcher()->reg(this, TRACK_CHANGE_FORMAT_QUERY, this, &TrackeditActionsController::setTrackFormat);
    dispatcher()->reg(this, TRACK_CHANGE_RATE_QUERY, this, &TrackeditActionsController::setTrackRate);

    dispatcher()->reg(this, SET_TRACK_VIEW_WAVEFORM, this, &TrackeditActionsController::changeTrackViewToWaveform);
    dispatcher()->reg(this, SET_TRACK_VIEW_SPECTROGRAM, this, &TrackeditActionsController::changeTrackViewToSpectrogram);
    dispatcher()->reg(this, SET_TRACK_VIEW_MULTI, this, &TrackeditActionsController::changeTrackViewToWaveformAndSpectrogram);
    dispatcher()->reg(this, TRACK_OPEN_SPECTROGRAM_SETTINGS, this, &TrackeditActionsController::openTrackSpectrogramSettings);

    dispatcher()->reg(this, LABEL_ADD_CODE, this, &TrackeditActionsController::addLabel);

    dispatcher()->reg(this, LABEL_DELETE_CODE, this, &TrackeditActionsController::labelDelete);
    dispatcher()->reg(this, LABEL_DELETE_MULTI_CODE, this, &TrackeditActionsController::labelDeleteMulti);

    dispatcher()->reg(this, LABEL_CUT_CODE, this, &TrackeditActionsController::labelCut);
    dispatcher()->reg(this, LABEL_CUT_MULTI_CODE, this, &TrackeditActionsController::labelCutMulti);

    dispatcher()->reg(this, LABEL_COPY_CODE, this, &TrackeditActionsController::labelCopy);
    dispatcher()->reg(this, LABEL_COPY_MULTI_CODE, this, &TrackeditActionsController::labelCopyMulti);

    projectHistory()->historyChanged().onNotify(this, [this]() {
        notifyActionEnabledChanged(TRACKEDIT_UNDO);
        notifyActionEnabledChanged(TRACKEDIT_REDO);
    });

    globalContext()->isRecordingChanged().onNotify(this, [this]() {
        for (const auto& actionCode : actionsDisabledDuringRecording) {
            notifyActionEnabledChanged(actionCode);
        }
    });

    selectionController()->clipsSelected().onReceive(this, [this](const trackedit::ClipKeyList&) {
        notifyActionEnabledChanged(GROUP_CLIPS_CODE);
        notifyActionEnabledChanged(UNGROUP_CLIPS_CODE);
    });
}

bool TrackeditActionsController::actionEnabled(const muse::actions::ActionCode& actionCode) const
{
    if (!canReceiveAction(actionCode)) {
        return false;
    }

    return true;
}

muse::async::Channel<muse::actions::ActionCode> TrackeditActionsController::actionEnabledChanged() const
{
    return m_actionEnabledChanged;
}

void TrackeditActionsController::notifyActionEnabledChanged(const ActionCode& actionCode)
{
    m_actionEnabledChanged.send(actionCode);
}

void TrackeditActionsController::notifyActionCheckedChanged(const ActionCode& actionCode)
{
    m_actionCheckedChanged.send(actionCode);
}

void TrackeditActionsController::doGlobalCopy()
{
    if (selectionController()->timeSelectionIsNotEmpty()) {
        dispatcher()->dispatch(RANGE_SELECTION_COPY_CODE);
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        dispatcher()->dispatch(MULTI_CLIP_COPY_CODE);
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        dispatcher()->dispatch(LABEL_COPY_MULTI_CODE);
        return;
    }
}

void TrackeditActionsController::doGlobalCut()
{
    const bool isTrackSelected = !selectionController()->timeSelectionIsNotEmpty() && !selectionController()->selectedTracks().empty()
                                 && !selectionController()->hasSelectedClips() && !selectionController()->hasSelectedLabels();

    const bool wasSet = configuration()->deleteBehavior() != DeleteBehavior::NotSet;

    if (!isTrackSelected) {
        if (!wasSet && !m_deleteBehaviorOnboardingScenario.showOnboardingDialog()) {
            return;
        }

        muse::Defer showFollowup([this, wasSet]() {
            if (!wasSet) {
                m_deleteBehaviorOnboardingScenario.showFollowupDialog();
            }
        });

        const DeleteBehavior deleteBehavior = configuration()->deleteBehavior();
        IF_ASSERT_FAILED(deleteBehavior != DeleteBehavior::NotSet) {
            return;
        }

        if (deleteBehavior != DeleteBehavior::NotSet && deleteBehavior != DeleteBehavior::LeaveGap) {
            switch (configuration()->closeGapBehavior()) {
            case CloseGapBehavior::ClipRipple:
                doGlobalCutPerClipRipple();
                break;
            case CloseGapBehavior::TrackRipple:
                doGlobalCutPerTrackRipple();
                break;
            case CloseGapBehavior::AllTracksRipple:
                doGlobalCutAllTracksRipple();
                break;
            default:
                LOGE() << "Unexpected close gap behavior: " << static_cast<int>(configuration()->closeGapBehavior());
                assert(false);
            }
            return;
        }
    }

    if (selectionController()->timeSelectionIsNotEmpty()) {
        auto selectedTracks = selectionController()->selectedTracks();
        secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
        secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

        dispatcher()->dispatch(RANGE_SELECTION_SPLIT_CUT,
                               ActionData::make_arg3<TrackIdList, secs_t, secs_t>(selectedTracks, selectedStartTime, selectedEndTime));
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        dispatcher()->dispatch(MULTI_CLIP_CUT_CODE, ActionData::make_arg1(false));
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        dispatcher()->dispatch(LABEL_CUT_MULTI_CODE, ActionData::make_arg1(false));
        return;
    }
}

void TrackeditActionsController::doGlobalCutPerClipRipple()
{
    auto moveClips = ActionData::make_arg1(false);
    if (selectionController()->timeSelectionIsNotEmpty()) {
        dispatcher()->dispatch(RANGE_SELECTION_CUT_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        dispatcher()->dispatch(MULTI_CLIP_CUT_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        dispatcher()->dispatch(LABEL_CUT_MULTI_CODE, moveClips);
        return;
    }
}

void TrackeditActionsController::doGlobalCutPerTrackRipple()
{
    auto moveClips = ActionData::make_arg1(true);
    if (selectionController()->timeSelectionIsNotEmpty()) {
        dispatcher()->dispatch(RANGE_SELECTION_CUT_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        dispatcher()->dispatch(MULTI_CLIP_CUT_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        dispatcher()->dispatch(LABEL_CUT_MULTI_CODE, moveClips);
        return;
    }
}

void TrackeditActionsController::doGlobalCutAllTracksRipple()
{
    auto moveClips = ActionData::make_arg1(true);

    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto tracks = project->trackeditProject()->trackIdList();

    if (selectionController()->timeSelectionIsNotEmpty()) {
        secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
        secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

        trackeditInteraction()->clearClipboard();
        trackeditInteraction()->cutItemDataIntoClipboard(tracks, selectedStartTime, selectedEndTime, true);

        selectionController()->resetDataSelection();
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        secs_t selectedStartTime = selectionController()->leftMostSelectedClipStartTime();
        secs_t selectedEndTime = selectionController()->rightMostSelectedClipEndTime();

        trackeditInteraction()->clearClipboard();
        trackeditInteraction()->cutItemDataIntoClipboard(tracks, selectedStartTime, selectedEndTime, true);

        selectionController()->resetSelectedClips();
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        secs_t selectedStartTime = selectionController()->leftMostSelectedLabelStartTime();
        secs_t selectedEndTime = selectionController()->rightMostSelectedLabelEndTime();

        trackeditInteraction()->clearClipboard();
        trackeditInteraction()->cutItemDataIntoClipboard(tracks, selectedStartTime, selectedEndTime, true);

        selectionController()->resetSelectedLabels();
        return;
    }
}

void TrackeditActionsController::doGlobalDelete()
{
    const bool isTrackSelected = !selectionController()->timeSelectionIsNotEmpty() && !selectionController()->selectedTracks().empty()
                                 && !selectionController()->hasSelectedClips() && !selectionController()->hasSelectedLabels();

    const bool wasSet = configuration()->deleteBehavior() != DeleteBehavior::NotSet;

    if (!isTrackSelected) {
        if (!wasSet && !m_deleteBehaviorOnboardingScenario.showOnboardingDialog()) {
            return;
        }

        muse::Defer showFollowup([this, wasSet]() {
            if (!wasSet) {
                m_deleteBehaviorOnboardingScenario.showFollowupDialog();
            }
        });

        const DeleteBehavior deleteBehavior = configuration()->deleteBehavior();
        IF_ASSERT_FAILED(deleteBehavior != DeleteBehavior::NotSet) {
            return;
        }

        if (deleteBehavior != DeleteBehavior::LeaveGap) {
            switch (configuration()->closeGapBehavior()) {
            case CloseGapBehavior::ClipRipple:
                doGlobalDeletePerClipRipple();
                break;
            case CloseGapBehavior::TrackRipple:
                doGlobalDeletePerTrackRipple();
                break;
            case CloseGapBehavior::AllTracksRipple:
                doGlobalDeleteAllTracksRipple();
                break;
            default:
                LOGE() << "Unexpected close gap behavior: " << static_cast<int>(configuration()->closeGapBehavior());
                assert(false);
            }
            return;
        }
    }

    if (selectionController()->timeSelectionIsNotEmpty()) {
        auto selectedTracks = selectionController()->selectedTracks();
        secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
        secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

        dispatcher()->dispatch(RANGE_SELECTION_SPLIT_DELETE,
                               ActionData::make_arg3<TrackIdList, secs_t, secs_t>(selectedTracks, selectedStartTime, selectedEndTime));
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        dispatcher()->dispatch(MULTI_CLIP_DELETE_CODE, ActionData::make_arg1(false));

        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        dispatcher()->dispatch(LABEL_DELETE_MULTI_CODE, ActionData::make_arg1(false));
        return;
    }

    if (!selectionController()->selectedTracks().empty()) {
        dispatcher()->dispatch(TRACK_DELETE);
        return;
    }

    interactive()->errorSync(muse::trc("trackedit", "No audio selected"),
                             muse::trc("trackedit", "Select the audio for Delete then try again."));
}

void TrackeditActionsController::doGlobalCancel()
{
    trackeditInteraction()->notifyAboutCancelDragEdit();
}

void TrackeditActionsController::doGlobalDeletePerClipRipple()
{
    auto moveClips = ActionData::make_arg1(false);

    if (selectionController()->timeSelectionIsNotEmpty()) {
        dispatcher()->dispatch(RANGE_SELECTION_DELETE_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        dispatcher()->dispatch(MULTI_CLIP_DELETE_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        dispatcher()->dispatch(LABEL_DELETE_MULTI_CODE, moveClips);
        return;
    }

    interactive()->errorSync(muse::trc("trackedit", "No audio selected"),
                             muse::trc("trackedit", "Select the audio for Delete then try again."));
}

void TrackeditActionsController::doGlobalDeletePerTrackRipple()
{
    auto moveClips = ActionData::make_arg1(true);

    if (selectionController()->timeSelectionIsNotEmpty()) {
        dispatcher()->dispatch(RANGE_SELECTION_DELETE_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        dispatcher()->dispatch(MULTI_CLIP_DELETE_CODE, moveClips);
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        dispatcher()->dispatch(LABEL_DELETE_MULTI_CODE, moveClips);
        return;
    }

    interactive()->errorSync(muse::trc("trackedit", "No audio selected"),
                             muse::trc("trackedit", "Select the audio for Delete then try again."));
}

void TrackeditActionsController::doGlobalDeleteAllTracksRipple()
{
    auto moveClips = ActionData::make_arg1(true);

    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto tracks = project->trackeditProject()->trackIdList();

    if (selectionController()->timeSelectionIsNotEmpty()) {
        secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
        secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

        trackeditInteraction()->removeTracksData(tracks, selectedStartTime, selectedEndTime, true);

        selectionController()->resetDataSelection();
        return;
    }

    if (!selectionController()->selectedClips().empty()) {
        secs_t selectedStartTime = selectionController()->leftMostSelectedClipStartTime();
        secs_t selectedEndTime = selectionController()->rightMostSelectedClipEndTime();

        trackeditInteraction()->removeTracksData(tracks, selectedStartTime, selectedEndTime, true);

        selectionController()->resetDataSelection();
        return;
    }

    if (!selectionController()->selectedLabels().empty()) {
        secs_t selectedStartTime = selectionController()->leftMostSelectedLabelStartTime();
        secs_t selectedEndTime = selectionController()->rightMostSelectedLabelEndTime();

        trackeditInteraction()->removeTracksData(tracks, selectedStartTime, selectedEndTime, true);

        selectionController()->resetDataSelection();
        return;
    }

    interactive()->errorSync(muse::trc("trackedit", "No audio selected"),
                             muse::trc("trackedit", "Select the audio for Delete then try again."));
}

void TrackeditActionsController::doGlobalSplit()
{
    TrackIdList tracksIdsToSplit = selectionController()->selectedTracks();

    if (tracksIdsToSplit.empty()) {
        return;
    }

    std::vector<secs_t> pivots;
    if (selectionController()->timeSelectionIsNotEmpty()) {
        pivots.push_back(selectionController()->dataSelectedStartTime());
        pivots.push_back(selectionController()->dataSelectedEndTime());
    } else {
        pivots.push_back(playbackState()->playbackPosition());
    }

    dispatcher()->dispatch(TRACK_SPLIT_AT, ActionData::make_arg2<TrackIdList, std::vector<secs_t> >(tracksIdsToSplit, pivots));
}

void TrackeditActionsController::doGlobalSplitIntoNewTrack()
{
    if (selectionController()->timeSelectionIsNotEmpty()) {
        TrackIdList selectedTracks = selectionController()->selectedTracks();
        secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
        secs_t selectedEndTime = selectionController()->dataSelectedEndTime();
        dispatcher()->dispatch(SPLIT_RANGE_SELECTION_INTO_NEW_TRACKS,
                               ActionData::make_arg3<TrackIdList, secs_t, secs_t>(selectedTracks, selectedStartTime, selectedEndTime));
        return;
    }

    ClipKeyList selectedClips = selectionController()->selectedClips();
    if (selectedClips.empty()) {
        return;
    }

    dispatcher()->dispatch(SPLIT_CLIPS_INTO_NEW_TRACKS, ActionData::make_arg1<ClipKeyList>(selectedClips));
}

void TrackeditActionsController::doGlobalJoin()
{
    auto selectedTracks = selectionController()->selectedTracks();
    secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
    secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

    dispatcher()->dispatch(MERGE_SELECTED_ON_TRACK,
                           ActionData::make_arg3<TrackIdList, secs_t, secs_t>(selectedTracks, selectedStartTime, selectedEndTime));
}

void TrackeditActionsController::doGlobalDisjoin()
{
    if (selectionController()->timeSelectionIsNotEmpty()) {
        TrackIdList selectedTracks = selectionController()->selectedTracks();
        secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
        secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

        dispatcher()->dispatch(SPLIT_RANGE_SELECTION_AT_SILENCES,
                               ActionData::make_arg3<TrackIdList, secs_t, secs_t>(selectedTracks, selectedStartTime, selectedEndTime));
        return;
    }

    ClipKeyList selectedClips = selectionController()->selectedClips();
    if (selectedClips.empty()) {
        return;
    }
    dispatcher()->dispatch(SPLIT_CLIPS_AT_SILENCES, ActionData::make_arg1<ClipKeyList>(selectedClips));
}

void TrackeditActionsController::undo()
{
    trackeditInteraction()->undo();
}

void TrackeditActionsController::redo()
{
    trackeditInteraction()->redo();
}

void TrackeditActionsController::doGlobalDuplicate()
{
    const auto selectedTracks = selectionController()->selectedTracks();

    if (!selectedTracks.empty()) {
        const auto selectedClips = selectionController()->selectedClips();
        if (selectedClips.empty()) {
            secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
            secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

            //If no range is selected, duplicate all content of the selected tracks
            //Otherwise, duplicate only the selected range
            (selectedStartTime == selectedEndTime)
            ? dispatcher()->dispatch(TRACK_DUPLICATE_CODE, ActionData::make_arg1<TrackIdList>(selectedTracks))
            : dispatcher()->dispatch(DUPLICATE_RANGE_SELECTION_CODE,
                                     ActionData::make_arg3<TrackIdList, secs_t, secs_t>(selectedTracks, selectedStartTime,
                                                                                        selectedEndTime));
        } else {
            dispatcher()->dispatch(DUPLICATE_CLIPS_CODE, ActionData::make_arg1<ClipKeyList>(selectedClips));
        }
    }
}

void TrackeditActionsController::clipCut(const ActionData& args)
{
    ClipKey clipKey = args.arg<ClipKey>(0);
    if (!clipKey.isValid()) {
        return;
    }

    trackeditInteraction()->clearClipboard();
    trackeditInteraction()->cutClipIntoClipboard(clipKey);
}

void TrackeditActionsController::clipCopy(const ActionData& args)
{
    ClipKey clipKey = args.arg<ClipKey>(0);
    if (!clipKey.isValid()) {
        return;
    }

    trackeditInteraction()->clearClipboard();
    trackeditInteraction()->copyClipIntoClipboard(clipKey);
}

void TrackeditActionsController::clipDelete(const ActionData& args)
{
    ClipKey clipKey = args.arg<ClipKey>(0);
    if (!clipKey.isValid()) {
        return;
    }

    selectionController()->resetSelectedClips();

    trackeditInteraction()->removeClip(clipKey);
}

void TrackeditActionsController::multiClipDelete(const ActionData& args)
{
    bool moveClips = false;

    if (args.count() >= 1) {
        moveClips = args.arg<bool>(0);
    }

    ClipKeyList selectedClipKeys = selectionController()->selectedClips();
    if (selectedClipKeys.empty()) {
        return;
    }

    selectionController()->resetSelectedClips();

    trackeditInteraction()->removeClips(selectedClipKeys, moveClips);
}

void TrackeditActionsController::labelDelete(const ActionData& args)
{
    LabelKey labelKey = args.arg<LabelKey>(0);
    if (!labelKey.isValid()) {
        return;
    }

    selectionController()->resetSelectedLabels();

    trackeditInteraction()->removeLabel(labelKey);
}

void TrackeditActionsController::labelDeleteMulti(const ActionData& args)
{
    bool moveLabels = false;

    if (args.count() >= 1) {
        moveLabels = args.arg<bool>(0);
    }

    LabelKeyList selectedLabelKeys = selectionController()->selectedLabels();
    if (selectedLabelKeys.empty()) {
        return;
    }

    selectionController()->resetSelectedLabels();

    trackeditInteraction()->removeLabels(selectedLabelKeys, moveLabels);
}

void TrackeditActionsController::labelCut(const ActionData& args)
{
    LabelKey labelKey = args.arg<LabelKey>(0);
    if (!labelKey.isValid()) {
        return;
    }

    selectionController()->resetSelectedLabels();

    trackeditInteraction()->cutLabel(labelKey);
}

void TrackeditActionsController::labelCutMulti(const muse::actions::ActionData& args)
{
    bool moveClips = false;

    if (args.count() >= 1) {
        moveClips = args.arg<bool>(0);
    }

    auto selectedLabelKeys = selectionController()->selectedLabels();
    if (selectedLabelKeys.empty()) {
        return;
    }

    trackeditInteraction()->clearClipboard();
    labelCopyMulti();
    selectionController()->resetSelectedLabels();

    trackeditInteraction()->removeLabels(selectedLabelKeys, moveClips);
}

void TrackeditActionsController::labelCopy(const ActionData& args)
{
    LabelKey labelKey = args.arg<LabelKey>(0);
    if (!labelKey.isValid()) {
        return;
    }

    trackeditInteraction()->clearClipboard();
    trackeditInteraction()->copyLabel(labelKey);
}

void TrackeditActionsController::labelCopyMulti()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto selectedTracks = selectionController()->selectedTracks();
    auto selectedLabels = selectionController()->selectedLabels();
    auto tracks = project->trackeditProject()->trackList();

    trackeditInteraction()->clearClipboard();

    secs_t offset = 0.0;
    std::optional<secs_t> leftmostLabelStartTime = trackeditInteraction()->getLeftmostLabelStartTime(selectedLabels);
    if (leftmostLabelStartTime.has_value()) {
        offset = -leftmostLabelStartTime.value();
    }

    for (const auto& track : tracks) {
        if (std::find(selectedTracks.begin(), selectedTracks.end(), track.id) == selectedTracks.end()) {
            continue;
        }

        LabelKeyList selectedTrackLabels;
        for (const auto& label : selectedLabels) {
            if (label.trackId == track.id) {
                selectedTrackLabels.push_back(label);
            }
        }

        trackeditInteraction()->copyNonContinuousTrackDataIntoClipboard(track.id, selectedTrackLabels, offset);
    }
}

void TrackeditActionsController::multiClipCut(const ActionData& args)
{
    bool moveClips = false;

    if (args.count() >= 1) {
        moveClips = args.arg<bool>(0);
    }

    auto selectedClips = selectionController()->selectedClips();
    if (selectedClips.empty()) {
        return;
    }

    trackeditInteraction()->clearClipboard();
    multiClipCopy();
    selectionController()->resetSelectedClips();

    trackeditInteraction()->removeClips(selectedClips, moveClips);
}

void TrackeditActionsController::rangeSelectionCut(const ActionData& args)
{
    bool moveClips = false;

    if (args.count() >= 1) {
        moveClips = args.arg<bool>(0);
    }

    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto selectedTracks = selectionController()->selectedTracks();
    secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
    secs_t selectedEndTime = selectionController()->dataSelectedEndTime();

    trackeditInteraction()->clearClipboard();
    trackeditInteraction()->cutItemDataIntoClipboard(selectedTracks, selectedStartTime, selectedEndTime, moveClips);

    selectionController()->resetDataSelection();
}

void TrackeditActionsController::multiClipCopy()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto selectedTracks = selectionController()->selectedTracks();
    auto selectedClips = selectionController()->selectedClips();
    auto tracks = project->trackeditProject()->trackList();

    trackeditInteraction()->clearClipboard();

    secs_t offset = 0.0;
    std::optional<secs_t> leftmostClipStartTime = trackeditInteraction()->getLeftmostClipStartTime(selectionController()->selectedClips());
    if (leftmostClipStartTime.has_value()) {
        offset = -leftmostClipStartTime.value();
    }

    for (const auto& track : tracks) {
        if (std::find(selectedTracks.begin(), selectedTracks.end(), track.id) == selectedTracks.end()) {
            continue;
        }

        ClipKeyList selectedTrackClips;
        for (const auto& clip : selectedClips) {
            if (clip.trackId == track.id) {
                selectedTrackClips.push_back(clip);
            }
        }

        trackeditInteraction()->copyNonContinuousTrackDataIntoClipboard(track.id, selectedTrackClips, offset);
    }
}

void TrackeditActionsController::rangeSelectionCopy()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto selectedTracks = selectionController()->selectedTracks();
    secs_t selectedStartTime = selectionController()->dataSelectedStartTime();
    secs_t selectedEndTime = selectionController()->dataSelectedEndTime();
    auto tracks = project->trackeditProject()->trackList();

    trackeditInteraction()->clearClipboard();

    for (const auto& track : tracks) {
        if (std::find(selectedTracks.begin(), selectedTracks.end(), track.id) == selectedTracks.end()) {
            continue;
        }

        trackeditInteraction()->copyContinuousTrackDataIntoClipboard(track.id, selectedStartTime, selectedEndTime);
    }
}

void TrackeditActionsController::rangeSelectionDelete(const ActionData& args)
{
    bool moveClips = false;

    if (args.count() >= 1) {
        moveClips = args.arg<bool>(0);
    }

    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto selectedTracks = selectionController()->selectedTracks();
    auto selectedStartTime = selectionController()->dataSelectedStartTime();
    auto selectedEndTime = selectionController()->dataSelectedEndTime();

    if (selectedTracks.empty()) {
        return;
    }

    trackeditInteraction()->removeTracksData(selectedTracks, selectedStartTime, selectedEndTime, moveClips);

    selectionController()->resetDataSelection();
}

void TrackeditActionsController::pasteDefault()
{
    switch (configuration()->pasteBehavior()) {
    case PasteBehavior::PasteOverlap:
        dispatcher()->dispatch(TRACKEDIT_PASTE_OVERLAP_CODE);
        break;
    case PasteBehavior::PasteInsert:
        switch (configuration()->pasteInsertBehavior()) {
        case PasteInsertBehavior::PasteInsert:
            dispatcher()->dispatch(TRACKEDIT_PASTE_INSERT_CODE);
            break;
        case PasteInsertBehavior::PasteInsertRipple:
            dispatcher()->dispatch(TRACKEDIT_PASTE_INSERT_ALL_TRACKS_RIPPLE_CODE);
            break;
        }
        break;
    }
}

void TrackeditActionsController::pasteOverlap()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto tracks = project->trackeditProject()->trackList();
    const double selectedStartTime = playbackState()->playbackPosition();

    if (!tracks.empty() && selectedStartTime >= 0) {
        auto ret = trackeditInteraction()->pasteFromClipboard(selectedStartTime, false);
        if (!ret && !ret.text().empty()) {
            interactive()->error(muse::trc("trackedit", "Paste error"), ret.text());
        }
    }
}

void TrackeditActionsController::pasteInsert()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto tracks = project->trackeditProject()->trackList();
    const double selectedStartTime = playbackState()->playbackPosition();

    if (!tracks.empty() && selectedStartTime >= 0) {
        auto ret = trackeditInteraction()->pasteFromClipboard(selectedStartTime, true);
        if (!ret && !ret.text().empty()) {
            interactive()->error(muse::trc("trackedit", "Paste error"), ret.text());
        }
    }
}

void TrackeditActionsController::pasteInsertRipple()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto tracks = project->trackeditProject()->trackList();
    const double selectedStartTime = playbackState()->playbackPosition();

    if (!tracks.empty() && selectedStartTime >= 0) {
        auto ret = trackeditInteraction()->pasteFromClipboard(selectedStartTime, false, true);
        if (!ret && !ret.text().empty()) {
            interactive()->error(muse::trc("trackedit", "Paste error"), ret.text());
        }
    }
}

void TrackeditActionsController::trackSplit(const ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    TrackId trackIdToSplit = args.arg<TrackId>(0);
    if (trackIdToSplit == -1) {
        return;
    }

    const secs_t playbackPosition = playbackState()->playbackPosition();

    dispatcher()->dispatch(TRACK_SPLIT_AT, ActionData::make_arg2<TrackIdList, secs_t>({ trackIdToSplit }, playbackPosition));
}

void TrackeditActionsController::tracksSplitAt(const ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 2) {
        return;
    }

    TrackIdList tracksIds = args.arg<TrackIdList>(0);

    if (tracksIds.empty()) {
        return;
    }

    auto pivot = args.arg<std::vector<secs_t> >(1);

    trackeditInteraction()->splitTracksAt(tracksIds, pivot);
}

void TrackeditActionsController::splitClipsAtSilences(const ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    ClipKeyList clipKeyList = args.arg<ClipKeyList>(0);
    if (clipKeyList.empty()) {
        return;
    }

    trackeditInteraction()->splitClipsAtSilences(clipKeyList);
}

void TrackeditActionsController::splitRangeSelectionAtSilences(const ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 3) {
        return;
    }

    TrackIdList tracksIds = args.arg<TrackIdList>(0);
    if (tracksIds.empty()) {
        return;
    }

    secs_t begin = args.arg<secs_t>(1);
    secs_t end = args.arg<secs_t>(2);

    trackeditInteraction()->splitRangeSelectionAtSilences(tracksIds, begin, end);
}

void TrackeditActionsController::splitRangeSelectionIntoNewTracks(const ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 3) {
        return;
    }

    TrackIdList tracksIds = args.arg<TrackIdList>(0);
    if (tracksIds.empty()) {
        return;
    }

    secs_t begin = args.arg<secs_t>(1);
    secs_t end = args.arg<secs_t>(2);

    trackeditInteraction()->splitRangeSelectionIntoNewTracks(tracksIds, begin, end);
}

void TrackeditActionsController::splitClipsIntoNewTracks(const ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    ClipKeyList clipKeyList = args.arg<ClipKeyList>(0);
    if (clipKeyList.empty()) {
        return;
    }

    trackeditInteraction()->splitClipsIntoNewTracks(clipKeyList);
}

void TrackeditActionsController::mergeSelectedOnTrack(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 3) {
        return;
    }

    TrackIdList tracksIds = args.arg<TrackIdList>(0);
    if (tracksIds.empty()) {
        return;
    }

    secs_t begin = args.arg<secs_t>(1);
    secs_t end = args.arg<secs_t>(2);

    trackeditInteraction()->mergeSelectedOnTracks(tracksIds, begin, end);
}

void TrackeditActionsController::duplicateSelected(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 3) {
        return;
    }

    TrackIdList tracksIds = args.arg<TrackIdList>(0);
    if (tracksIds.empty()) {
        return;
    }

    secs_t begin = args.arg<secs_t>(1);
    secs_t end = args.arg<secs_t>(2);

    trackeditInteraction()->duplicateSelectedOnTracks(tracksIds, begin, end);
}

void TrackeditActionsController::duplicateClips(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    ClipKeyList clipKeyList = args.arg<ClipKeyList>(0);
    trackeditInteraction()->duplicateClips(clipKeyList);
}

void TrackeditActionsController::clipSplitCut(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    ClipKey clipKey = args.arg<ClipKey>(0);
    if (!clipKey.isValid()) {
        return;
    }

    trackeditInteraction()->clearClipboard();
    trackeditInteraction()->clipSplitCut(clipKey);
}

void TrackeditActionsController::clipSplitDelete(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    ClipKey clipKey = args.arg<ClipKey>(0);
    if (!clipKey.isValid()) {
        return;
    }

    trackeditInteraction()->clipSplitDelete(clipKey);
}

void TrackeditActionsController::splitCutSelected(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 3) {
        return;
    }

    TrackIdList tracksIds = args.arg<TrackIdList>(0);
    if (tracksIds.empty()) {
        return;
    }

    secs_t begin = args.arg<secs_t>(1);
    secs_t end = args.arg<secs_t>(2);

    trackeditInteraction()->clearClipboard();
    trackeditInteraction()->splitCutSelectedOnTracks(tracksIds, begin, end);

    selectionController()->resetDataSelection();
}

void TrackeditActionsController::splitDeleteSelected(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 3) {
        return;
    }

    TrackIdList tracksIds = args.arg<TrackIdList>(0);
    if (tracksIds.empty()) {
        return;
    }

    secs_t begin = args.arg<secs_t>(1);
    secs_t end = args.arg<secs_t>(2);

    trackeditInteraction()->splitDeleteSelectedOnTracks(tracksIds, begin, end);

    selectionController()->resetDataSelection();
}

void TrackeditActionsController::newMonoTrack()
{
    trackeditInteraction()->newMonoTrack();
}

void TrackeditActionsController::newStereoTrack()
{
    trackeditInteraction()->newStereoTrack();
}

void TrackeditActionsController::newLabelTrack()
{
    trackeditInteraction()->newLabelTrack();
}

void TrackeditActionsController::deleteTracks(const muse::actions::ActionData&)
{
    TrackIdList trackIds = selectionController()->selectedTracks();

    if (trackIds.empty()) {
        return;
    }

    trackeditInteraction()->deleteTracks(trackIds);
}

void TrackeditActionsController::duplicateTracks(const muse::actions::ActionData&)
{
    TrackIdList trackIds = selectionController()->selectedTracks();

    if (trackIds.empty()) {
        return;
    }

    trackeditInteraction()->duplicateTracks(trackIds);
}

void TrackeditActionsController::moveTracksUp(const muse::actions::ActionData&)
{
    TrackIdList trackIds = selectionController()->selectedTracks();

    if (trackIds.empty()) {
        return;
    }

    trackeditInteraction()->moveTracks(trackIds, TrackMoveDirection::Up);
}

void TrackeditActionsController::moveTracksDown(const muse::actions::ActionData&)
{
    TrackIdList trackIds = selectionController()->selectedTracks();

    if (trackIds.empty()) {
        return;
    }

    trackeditInteraction()->moveTracks(trackIds, TrackMoveDirection::Down);
}

void TrackeditActionsController::moveTracksToTop(const muse::actions::ActionData&)
{
    TrackIdList trackIds = selectionController()->selectedTracks();

    if (trackIds.empty()) {
        return;
    }

    trackeditInteraction()->moveTracks(trackIds, TrackMoveDirection::Top);
}

void TrackeditActionsController::moveTracksToBottom(const muse::actions::ActionData&)
{
    TrackIdList trackIds = selectionController()->selectedTracks();

    if (trackIds.empty()) {
        return;
    }

    trackeditInteraction()->moveTracks(trackIds, TrackMoveDirection::Bottom);
}

void TrackeditActionsController::swapStereoChannels(const muse::actions::ActionData&)
{
    const TrackIdList trackIds = selectionController()->selectedTracks();
    if (trackIds.empty()) {
        return;
    }

    trackeditInteraction()->swapStereoChannels(trackIds);
}

void TrackeditActionsController::splitStereoToLR(const muse::actions::ActionData&)
{
    const auto project = globalContext()->currentProject();
    if (!project) {
        return;
    }

    const TrackIdList selectedTracks = selectionController()->selectedTracks();
    if (selectedTracks.empty()) {
        return;
    }

    auto tracks = project->trackeditProject()->trackList();
    std::vector<TrackId> tracksIdsToSplit;
    for (const auto& track : tracks) {
        if (std::find(selectedTracks.begin(), selectedTracks.end(), track.id) == selectedTracks.end()) {
            continue;
        }

        tracksIdsToSplit.push_back(track.id);
    }

    trackeditInteraction()->splitStereoTracksToLRMono(tracksIdsToSplit);
}

void TrackeditActionsController::splitStereoToCenter(const muse::actions::ActionData&)
{
    const auto project = globalContext()->currentProject();
    if (!project) {
        return;
    }

    const TrackIdList selectedTracks = selectionController()->selectedTracks();
    if (selectedTracks.empty()) {
        return;
    }

    auto tracks = project->trackeditProject()->trackList();
    std::vector<TrackId> tracksIdsToSplit;
    for (const auto& track : tracks) {
        if (std::find(selectedTracks.begin(), selectedTracks.end(), track.id) == selectedTracks.end()) {
            continue;
        }

        tracksIdsToSplit.push_back(track.id);
    }

    trackeditInteraction()->splitStereoTracksToCenterMono(tracksIdsToSplit);
}

void TrackeditActionsController::trimAudioOutsideSelection()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto selectedTracks = selectionController()->selectedTracks();
    auto selectedStartTime = selectionController()->dataSelectedStartTime();
    auto selectedEndTime = selectionController()->dataSelectedEndTime();
    auto tracks = project->trackeditProject()->trackList();

    std::vector<TrackId> tracksIdsToTrim;
    for (const auto& track : tracks) {
        if (std::find(selectedTracks.begin(), selectedTracks.end(), track.id) == selectedTracks.end()) {
            continue;
        }

        tracksIdsToTrim.push_back(track.id);
    }

    if (tracksIdsToTrim.empty()) {
        return;
    }

    trackeditInteraction()->trimTracksData(tracksIdsToTrim, selectedStartTime, selectedEndTime);
}

void TrackeditActionsController::silenceAudioSelection()
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    auto selectedTracks = selectionController()->selectedTracks();
    auto selectedStartTime = selectionController()->dataSelectedStartTime();
    auto selectedEndTime = selectionController()->dataSelectedEndTime();
    auto tracks = project->trackeditProject()->trackList();

    std::vector<TrackId> tracksIdsToSilence;
    for (const auto& track : tracks) {
        if (std::find(selectedTracks.begin(), selectedTracks.end(), track.id) == selectedTracks.end()) {
            continue;
        }

        tracksIdsToSilence.push_back(track.id);
    }

    if (tracksIdsToSilence.empty()) {
        return;
    }

    trackeditInteraction()->silenceTracksData(tracksIdsToSilence, selectedStartTime, selectedEndTime);
}

void TrackeditActionsController::toggleStretchClipToMatchTempo(const ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    trackedit::ClipKey clipKey = args.arg<trackedit::ClipKey>(0);
    if (!clipKey.isValid()) {
        return;
    }

    trackeditInteraction()->toggleStretchToMatchProjectTempo(clipKey);
    notifyActionCheckedChanged(STRETCH_ENABLED_CODE);
}

void TrackeditActionsController::openClipPitchAndSpeed()
{
    auto selectedClips = selectionController()->selectedClips();

    if (selectedClips.empty() || selectedClips.size() > 1) {
        return;
    }

    dispatcher()->dispatch("clip-pitch-speed", ActionData::make_arg1<trackedit::ClipKey>(selectedClips.front()));
}

void TrackeditActionsController::renderClipPitchAndSpeed(const muse::actions::ActionData& args)
{
    IF_ASSERT_FAILED(args.count() == 1) {
        return;
    }

    trackedit::ClipKey clipKey = args.arg<trackedit::ClipKey>(0);
    if (!clipKey.isValid()) {
        return;
    }

    // todo
    interactive()->showProgress(muse::trc("trackedit", "Applying"), trackeditInteraction()->progress());

    trackeditInteraction()->renderClipPitchAndSpeed(clipKey);
}

void TrackeditActionsController::groupClips()
{
    const auto selectedClips = selectionController()->selectedClips();

    trackeditInteraction()->groupClips(selectedClips);

    notifyActionEnabledChanged(GROUP_CLIPS_CODE);
    notifyActionEnabledChanged(UNGROUP_CLIPS_CODE);
}

void TrackeditActionsController::ungroupClips()
{
    trackeditInteraction()->ungroupClips(selectionController()->selectedClips());

    notifyActionEnabledChanged(GROUP_CLIPS_CODE);
    notifyActionEnabledChanged(UNGROUP_CLIPS_CODE);
}

void TrackeditActionsController::selectAll()
{
    selectionController()->setSelectedAllAudioData();
}

void TrackeditActionsController::selectNone()
{
    selectionController()->resetTimeSelection();
    selectionController()->resetDataSelection();
    selectionController()->resetSelectedClips();
    selectionController()->resetSelectedTracks();
}

void TrackeditActionsController::selectAllTracks()
{
    auto prj = globalContext()->currentTrackeditProject();
    if (!prj) {
        return;
    }

    selectionController()->setSelectedTracks(prj->trackIdList());
}

void TrackeditActionsController::selectLeftOfPlaybackPos()
{
    RetVal<Val> rv = interactive()->openSync("audacity://trackedit/custom_time");
    if (!rv.ret) {
        return;
    }

    secs_t playbackTime = playbackState()->playbackPosition();

    if (muse::RealIsEqualOrMore(rv.val.toDouble(), playbackTime)) {
        return;
    }

    selectionController()->setDataSelectedStartTime(rv.val.toDouble(), true);
    selectionController()->setDataSelectedEndTime(playbackTime, true);
}

void TrackeditActionsController::selectRightOfPlaybackPos()
{
    RetVal<Val> rv = interactive()->openSync("audacity://trackedit/custom_time");
    if (!rv.ret) {
        return;
    }

    secs_t playbackTime = playbackState()->playbackPosition();
    secs_t rightOfPlaybackValue = rv.val.toDouble();
    if (muse::RealIsEqualOrLess(rightOfPlaybackValue, playbackTime)) {
        return;
    }

    //! NOTE: clamp to 2x size of project
    project::IAudacityProjectPtr prj = globalContext()->currentProject();
    if (!prj) {
        return;
    }

    secs_t maxTime = prj->trackeditProject()->totalTime().to_double() * 2;
    rightOfPlaybackValue = std::min(rightOfPlaybackValue, maxTime);

    selectionController()->setDataSelectedStartTime(playbackTime, true);
    selectionController()->setDataSelectedEndTime(rightOfPlaybackValue, true);
}

void TrackeditActionsController::selectTrackStartToCursor()
{
    ClipKeyList clipsOnSelectedTracks;
    for (const auto& track : selectionController()->selectedTracks()) {
        for (const auto& clip : trackeditInteraction()->clipsOnTrack(track)) {
            clipsOnSelectedTracks.push_back(clip);
        }
    }

    std::optional<secs_t> leftmostClipStartTime = trackeditInteraction()->getLeftmostClipStartTime(clipsOnSelectedTracks);
    if (leftmostClipStartTime.has_value()) {
        selectionController()->setDataSelectedStartTime(leftmostClipStartTime.value(), true);
    } else {
        selectionController()->setDataSelectedStartTime(0.0, true);
    }

    selectionController()->setDataSelectedEndTime(playbackState()->playbackPosition(), true);
}

void TrackeditActionsController::selectCursorToTrackEnd()
{
    ClipKeyList clipsOnSelectedTracks;
    for (const auto& track : selectionController()->selectedTracks()) {
        for (const auto& clip : trackeditInteraction()->clipsOnTrack(track)) {
            clipsOnSelectedTracks.push_back(clip);
        }
    }

    std::optional<secs_t> rightmostClipEndTime = trackeditInteraction()->getRightmostClipEndTime(clipsOnSelectedTracks);
    if (rightmostClipEndTime.has_value()) {
        selectionController()->setDataSelectedStartTime(playbackState()->playbackPosition(), true);
        selectionController()->setDataSelectedEndTime(rightmostClipEndTime.value(), true);
    } else {
        //! NOTE: AU3 behavior
        selectTrackStartToCursor();
    }
}

void TrackeditActionsController::selectTrackStartToEnd()
{
    ClipKeyList clipsOnSelectedTracks;
    for (const auto& track : selectionController()->selectedTracks()) {
        for (const auto& clip : trackeditInteraction()->clipsOnTrack(track)) {
            clipsOnSelectedTracks.push_back(clip);
        }
    }

    std::optional<secs_t> leftmostClipStartTime = trackeditInteraction()->getLeftmostClipStartTime(clipsOnSelectedTracks);
    std::optional<secs_t> rightmostClipEndTime = trackeditInteraction()->getRightmostClipEndTime(clipsOnSelectedTracks);

    if (leftmostClipStartTime.has_value() && rightmostClipEndTime.has_value()) {
        selectionController()->setDataSelectedStartTime(leftmostClipStartTime.value(), true);
        selectionController()->setDataSelectedEndTime(rightmostClipEndTime.value(), true);
    }
}

void TrackeditActionsController::moveCursorToClosestZeroCrossing()
{
    secs_t zeroCrossing = trackeditInteraction()->nearestZeroCrossing(playbackState()->playbackPosition());
    zeroCrossing = std::max(zeroCrossing.to_double(), 0.0);

    muse::actions::ActionQuery q(PLAYBACK_SEEK_QUERY);
    q.addParam("seekTime", muse::Val(zeroCrossing));
    q.addParam("triggerPlay", muse::Val(false));
    dispatcher()->dispatch(q);
}

void TrackeditActionsController::setClipColor(const muse::actions::ActionQuery& q)
{
    if (!selectionController()->hasSelectedClips()) {
        return;
    }

    std::string newColor;
    if (q.contains("color")) {
        newColor = q.param("color").toString();
    } else {
        newColor = "";
    }

    auto clipKey = selectionController()->selectedClips().front();
    trackeditInteraction()->changeClipColor(clipKey, newColor);
    notifyActionCheckedChanged(q.toString());
}

void TrackeditActionsController::setTrackColor(const muse::actions::ActionQuery& q)
{
    const auto tracks = selectionController()->selectedTracks();
    if (tracks.empty()) {
        return;
    }

    std::string color;
    if (q.contains("color")) {
        color = q.param("color").toString();
    } else {
        color = "";
    }

    trackeditInteraction()->changeTracksColor(tracks, color);
    notifyActionCheckedChanged(q.toString());
}

void TrackeditActionsController::setTrackFormat(const muse::actions::ActionQuery& q)
{
    const auto tracks = selectionController()->selectedTracks();
    if (tracks.empty()) {
        return;
    }

    if (!q.contains("format")) {
        return;
    }

    const int format = q.param("format").toInt();
    if (trackeditInteraction()->changeTracksFormat(tracks, static_cast<TrackFormat>(format))) {
        notifyActionCheckedChanged(q.toString());
    }
}

void TrackeditActionsController::setCustomTrackRate(const muse::actions::ActionData&)
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    if (!project) {
        return;
    }

    const TrackIdList tracks = selectionController()->selectedTracks();
    if (tracks.empty()) {
        return;
    }

    const TrackId focusedTrackId = selectionController()->focusedTrack();
    const std::optional<Track> focused = project->trackeditProject()->track(focusedTrackId);
    if (!focused) {
        return;
    }

    muse::UriQuery customRateUri("audacity://trackedit/custom_rate");
    customRateUri.addParam("title", muse::Val(muse::trc("trackedit", "Set rate")));
    customRateUri.addParam("rate", muse::Val(static_cast<int>(focused.value().rate)));

    RetVal<Val> rv = interactive()->openSync(customRateUri);
    if (rv.ret.code() != static_cast<int>(Ret::Code::Ok)) {
        return;
    }

    const auto customRate = rv.val.toInt();
    if (customRate <= 0) {
        return;
    }

    trackeditInteraction()->changeTracksRate(tracks, customRate);
}

void TrackeditActionsController::setTrackRate(const muse::actions::ActionQuery& q)
{
    const auto tracks = selectionController()->selectedTracks();
    if (tracks.empty()) {
        return;
    }

    if (!q.contains("rate")) {
        return;
    }

    const int rate = q.param("rate").toInt();
    if (trackeditInteraction()->changeTracksRate(tracks, rate)) {
        notifyActionCheckedChanged(q.toString());
    }
}

void TrackeditActionsController::changeTrackViewToWaveform(const muse::actions::ActionQuery& q)
{
    changeTrackView(q, TrackViewType::Waveform);
}

void TrackeditActionsController::changeTrackViewToSpectrogram(const muse::actions::ActionQuery& q)
{
    changeTrackView(q, TrackViewType::Spectrogram);
}

void TrackeditActionsController::changeTrackViewToWaveformAndSpectrogram(const muse::actions::ActionQuery& q)
{
    changeTrackView(q, TrackViewType::WaveformAndSpectrogram);
}

void TrackeditActionsController::changeTrackView(const muse::actions::ActionQuery& q, TrackViewType trackView)
{
    IF_ASSERT_FAILED(q.params().size() == 1) {
        return;
    }
    const auto trackId = q.param("trackId").toInt();
    if (!trackeditInteraction()->changeAudioTrackViewType(trackId, trackView)) {
        return;
    }
    switch (trackView) {
    case TrackViewType::Waveform:
        notifyActionCheckedChanged(SET_TRACK_VIEW_WAVEFORM.toString());
        break;
    case TrackViewType::Spectrogram:
        notifyActionCheckedChanged(SET_TRACK_VIEW_SPECTROGRAM.toString());
        break;
    case TrackViewType::WaveformAndSpectrogram:
        notifyActionCheckedChanged(SET_TRACK_VIEW_MULTI.toString());
        break;
    default:
        assert(false);
    }
}

void TrackeditActionsController::openTrackSpectrogramSettings(const muse::actions::ActionQuery& q)
{
    muse::UriQuery spectrogramSettingsUri("audacity://trackedit/track_spectrogram_settings");
    spectrogramSettingsUri.addParam("trackId", muse::Val(q.param("trackId").toInt()));
    interactive()->open(spectrogramSettingsUri);
}

void TrackeditActionsController::addLabel()
{
    trackeditInteraction()->addLabelToSelection();
}

void TrackeditActionsController::makeStereoTrack(const muse::actions::ActionData&)
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    if (!project) {
        return;
    }

    const auto selectedTracks = selectionController()->selectedTracks();
    if (selectedTracks.size() != 1) {
        return;
    }

    const std::optional<Track> selectedTrack = project->trackeditProject()->track(selectionController()->selectedTracks().front());
    if (!selectedTrack || selectedTrack->type != TrackType::Mono) {
        return;
    }

    const TrackList tracks = project->trackeditProject()->trackList();
    const auto it = std::find_if(tracks.begin(), tracks.end(),
                                 [&selectedTrack](const Track& track) { return track.id == selectedTrack->id; });

    if (it == tracks.end()) {
        return;
    }

    const auto nextTrack = std::next(it);
    if ((nextTrack == tracks.end()) || nextTrack->type != TrackType::Mono) {
        return;
    }

    trackeditInteraction()->makeStereoTrack(selectedTrack->id, nextTrack->id);
}

void TrackeditActionsController::resampleTracks(const muse::actions::ActionData&)
{
    project::IAudacityProjectPtr project = globalContext()->currentProject();
    if (!project) {
        return;
    }

    const TrackIdList selectedTracks = selectionController()->selectedTracks();
    if (selectedTracks.size() == 0) {
        return;
    }

    const TrackId focusedTrackId = selectionController()->focusedTrack();
    const std::optional<Track> focused = project->trackeditProject()->track(focusedTrackId);
    if (!focused) {
        return;
    }

    muse::UriQuery resampleUri("audacity://trackedit/custom_rate");

    muse::ValList availableSampleRates;
    for (const auto& rate : audioDevicesProvider()->availableSampleRateList()) {
        availableSampleRates.push_back(muse::Val(static_cast<int>(rate)));
    }
    resampleUri.addParam("availableRates", muse::Val(availableSampleRates));
    resampleUri.addParam("title", muse::Val(muse::trc("trackedit", "Resample")));
    resampleUri.addParam("rate", muse::Val(static_cast<int>(focused.value().rate)));

    const RetVal<Val> rv = interactive()->openSync(resampleUri);
    if (rv.ret.code() != static_cast<int>(Ret::Code::Ok)) {
        return;
    }

    const int customRate = rv.val.toInt();
    if (customRate <= 0) {
        return;
    }

    trackeditInteraction()->resampleTracks(selectedTracks, customRate);
}

bool TrackeditActionsController::actionChecked(const ActionCode&) const
{
    //! TODO AU4
    return false;
}

Channel<ActionCode> TrackeditActionsController::actionCheckedChanged() const
{
    return m_actionCheckedChanged;
}

bool TrackeditActionsController::canReceiveAction(const ActionCode& actionCode) const
{
    if (globalContext()->currentProject() == nullptr) {
        return false;
    } else if (globalContext()->isRecording() && muse::contains(actionsDisabledDuringRecording, actionCode)) {
        return false;
    } else if (actionCode == TRACKEDIT_UNDO) {
        return trackeditInteraction()->canUndo();
    } else if (actionCode == TRACKEDIT_REDO) {
        return trackeditInteraction()->canRedo();
    } else if (actionCode == GROUP_CLIPS_CODE) {
        return selectionController()->selectedClips().size() > 1 && !selectionController()->isSelectionGrouped();
    } else if (actionCode == UNGROUP_CLIPS_CODE) {
        return selectionController()->selectedClips().size() > 1 && selectionController()->selectionContainsGroup();
    }

    return true;
}

au::context::IPlaybackStatePtr TrackeditActionsController::playbackState() const
{
    return globalContext()->playbackState();
}
